package io.gitee.zhangbinhub.admin.component

import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.*
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames
import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestClient

class AcpOpaqueTokenIntrospect(
    private val introspectionUri: String,
    private val restClient: RestClient,
    private val tokenTools: TokenTools
) : OpaqueTokenIntrospector {
    private val stringObjectMap: ParameterizedTypeReference<MutableMap<String, Any?>> =
        object : ParameterizedTypeReference<MutableMap<String, Any?>>() {}

    @Throws(OAuth2IntrospectionException::class)
    override fun introspect(token: String?): OAuth2AuthenticatedPrincipal =
        tokenTools.convertClaimsSet(
            adaptToNimbusResponse(
                makeRequest(
                    token ?: throw OAuth2IntrospectionException("token is null")
                )
            )
        )

    @Throws(OAuth2IntrospectionException::class, BadOpaqueTokenException::class)
    private fun makeRequest(token: String): ResponseEntity<MutableMap<String, Any?>> = try {
        restClient.post()
            .uri(introspectionUri)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .accept(MediaType.APPLICATION_JSON)
            .body(LinkedMultiValueMap<String, String?>().apply {
                this.add("token", token)
            })
            .retrieve()
            .toEntity(stringObjectMap)
    } catch (ex: HttpClientErrorException) {
        throw BadOpaqueTokenException(ex.responseBodyAsString, ex)
    } catch (ex: Exception) {
        throw OAuth2IntrospectionException(ex.message, ex)
    }

    @Throws(OAuth2IntrospectionException::class, BadOpaqueTokenException::class)
    private fun adaptToNimbusResponse(responseEntity: ResponseEntity<MutableMap<String, Any?>>): MutableMap<String, Any?> {
        if (responseEntity.statusCode != HttpStatus.OK) {
            throw OAuth2IntrospectionException(
                "Introspection endpoint responded with " + responseEntity.statusCode
            )
        }
        val claims = (responseEntity.body ?: mapOf()).toMutableMap()
        val active = claims.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE) { _: String, value: Any? ->
            if (value is String) {
                return@compute value.toBoolean()
            }
            if (value is Boolean) {
                return@compute value
            }
            false
        } as Boolean
        if (!active) {
            throw BadOpaqueTokenException("Provided token isn't active")
        }
        return claims
    }
}